UE4.26运行时的Mesh生成与编辑
正文
一个经常出现的问题是如何在UE4游戏或应用程序中实现运行时网格创建。在过去,大家主要使用UProceduralMesh组件(API文档链接²)来实现。但自打UE4.25起,UE已经可以在运行时“生成”和更新UStaticMesh,然后这些UStaticMesh可以在UStaticMeshComponent/Actor中使用。所以现在的问题变成了你应该用哪一个?此外,还有第三方解决方案(如RuntimeMeshComponent³),它们提供的功能比UProceduralMesh组件更多,在某些情况下可能是更好的选择。(在本教程的其余部分,UProceduralMesh组件缩写为PMC,UStaticMesh组件缩写为SMC)。
不幸的是,没有”最佳”选项——这取决于你的需求。并且,如何将其和我们的GeometryProcessing插件结合使用的方法并不那么显而易见,GeometryProcessing插件使用FDynamicMesh3来表示网格,而和Actor以及Component没有任何关系。因此,我将在本教程中展示实现运行时生成和编辑Mesh的一种方法。
获取和运行示例项目
本教程的项目位于GitHub上的UnrealMeshProcessingTutorials代码仓库(MIT许可证⁴) 中的UE4.26/RuntimeGeometryDemo子文件夹中。一个坏消息是此项目目前只能在Windows上工作。但好消息是通过一些选择性的删减,它应该能跑在OSX或者Linux上的。我将在文章末尾介绍怎么做。如果你不想使用git获取示例项目,你可以通过以下链接直接下载项目的zip包。
进入RuntimeGeometryDemo最外层文件夹后,右键单击Windows资源管理器中的RuntimeGeometryDemo.uproject,然后从上下文菜单中选择“Generate Visual Studio project files”项目文件。这将生成RuntimeGeometryDemo.sln。你也可以直接在编辑器中打开.uproject(它会要求编译),但我想大概率你是需要参考下本教程的C++代码的。
生成解决方案并运行(按F5),UE编辑器应该会打开演示关卡。你可以使用主工具栏中的大大的Play按钮在PIE模式中测试项目,或者单击“Launch”按钮生成可执行文件。这会需要几分钟,之后生成的游戏将在单独的窗口中运行。在演示关卡里随便跑跑,向墙体射击!因为没有菜单/UI,你需要按Alt+F4来退出。
代码
PMC还是SMC?
这在 PMC 中相对直白些,我建议去看下ProceduralMeshComponent.cpp中实现它的那部分代码,FProceduralMedeSceneProxy类在文件头部。你会看到在其构造函数中FProceduralMeshSceneProxy在外部创建的FProcMeshSection做了转换并且初始化了FStaticMeshVertexBuffers和FLocalVertexFactory。这些结构保存了GPU渲染所需要的数据,如顶点位置、三角形索引缓冲区、法线和切线、UV、顶点色等。对于GPU来说,这些数据在PMC或SMC之间没有区别,唯一的区别是数据如何被添加到这些结构中。
SMC要复杂得多。我在这里用的术语不太严谨,因为UStaticMeshComponent不存储网格本身——它引用一个UStaticMesh,是UStaticMesh负责存储网格数据。在UE4.25之前,无法在运行时更新UStaticMesh。这是因为传统上,你的“源网格(SourceMesh)”在编辑器中以FMeshDescription的形式储存在UStaticMesh,它是通过“烘焙”生成的一个预处理,优化过的“渲染网格(rendering mesh)”,用它来初始化FStaticMeshSceneProxy(在StaticMeshRender.cpp中完成)。FMeshDescription在烘焙后不再被使用,因此在构建的游戏中会将它剥离。而这个烘焙过程,由UStaticMesh::Build()启动,依赖于各种仅能在编辑器模式下运行的函数和数据,这就是为什么你不能在运行时更新UStaticMesh几何体的原因。
但是,在4.25中添加了一个新函数——UstaticMesh::BuildFromMeshDescriptions()。此函数接受一组FMeshDescription作为输入并用其初始化渲染用的网格数据,这就允许我们在运行时生成UStaticMesh。运行时与编辑器模式下的生成路径不同——它跳过了在运行时执行速度过慢(例如生成距离场照明数据)或没有用处的各种复杂步骤(例如生成光照贴图UV,运行时无法烘焙光照贴图,所以这步在运行时毫无意义)。
调用BuildFromMeshDescriptions()比在ProceduralMeshComponent更新Sections更昂贵。权衡一下,你可以在多个StaticMeshComponent中复用生成的UStaticMesh(获得Instanced Rendering带来的好处),而如果你想要具有多个"相同"的PMC,则需要将Mesh复制到每个PMC中。此外,你还可以使用 SMC 获得额外的渲染功能。例如,PMC中使用的FProcMeshVertex仅支持4个UV通道,但UStaticMesh最多支持7个UV通道。UStaticMesh还支持LOD和Sockets,通常SMC在整个引擎中受到更好的支持。
另一个根本的区别在于两者使用渲染器的方式。PMC使用所谓的“动态绘制(Dynamic Draw)”绘制,这意味着每帧它要重新生成并提交FMeshBatches。这基本上等同于告诉渲染器“我的顶点/索引缓冲区在任何帧上可能会更改,所以不必费心缓存任何东西”,这导致一个性能成本。SMC使用“静态绘制(Static Draw)”,该路径告诉渲染器渲染缓冲区不会改变,因此它可以进行更激进的缓存和优化。如果你对此感兴趣,你就千万别错过Marcus Wassmer在GDC2019上就UE4当前的渲染管线做的技术分享。
第三个选项:
USimpleDynamicMeshComponent
这也就意味着PMC不适合直接用于任何类型的网格编辑。例如,你想用一个平面去切割一个立方体并填充切割面。用PMC也就意味着你要直接切割PMC的Sections,你手里的数据实际上不是一个立方体而是6个不相连的三角形补丁。因此在切割后你得到的不是一个对于洞填充操作很理想的封闭边界环,而是得到一些需要连接起来才能定义这个洞的3D线段。这对编辑的可靠性非常不利。类似地,如果你“拉”这个立方体的某个角,你只会创建一个裂缝。我还可以列举更多……相信我,这会是场噩梦。
因此,为了在UE4.25中实现中的网格建模编辑器模式,我们引入了另一种网格组件,USimpleDynamicMeshComponent。这个组件类似于PMC,因为它使用动态绘制(Dynamic Draw)路径,并且设计上就考虑了需要经常被更新的情况。然而,与PMC不同的是,它存储了一个更复杂的网格表示,支持在顶点处的属性拆分,并在内部处理了将网格数据重写为适合于GPU工作的格式。这个更复杂的网格表示是什么呢?自然是一个FDynamicMesh3。
为什么命名为“简单(Simple)”?因为还有另一种变体——UOctreeDynamicMeshComponent——它被设计用来高效地更新大网格的子区域。这超出了本教程的范围。
USimpleDynamicMeshComponent(简写为SDMC)有许多PMC所不具备的特性,这些特性非常适用于交互式网格编辑。例如,它支持对模型三角面的子集进行材质覆盖,或者隐藏它们,而不需要重新构造网格。它也有各种“快速更新”不同渲染缓冲区的功能。例如你只改变顶点的位置或者颜色或者法线。它还支持将网格“分块”到多个渲染缓冲区以快速更新,并且可以在网格更新时自动计算切线。但我并不打算在本教程中展示如何使用这些功能。
在取舍方面,SDMC将比PMC生成更大的渲染缓冲区。因此,如果GPU内存在你的动态几何编辑“游戏”是瓶颈,它可能不是最好的选择。不过,在交互网格编辑的情况下,这种内存使用与你将需要的许多网格副本和辅助数据结构相比,通常是微不足道的。SDMC目前也不支持任何物理解算。最后,它不能被序列化——你应该只在生成的网格会以其他形式保存的情况下使用SDMC。
运行时几何架构和ADynamicMeshBaseActor
架构的核心问题是,生成的网格来自何方?如果你是完全程序化地生成网格,或者是从文件中加载,那么你可以很容易地使用任何一个选项,唯一的问题是,它在生成之后是静态的,还是需要经常更新,或者你想“偷懒”不去自己构建Sections。
如果在网格生成之后需要更改它们,那么我强烈建议你不要将这些组件中的网格表示看作你应用程序数据模型的规范表示。对于初学者,它们都不会序列化你的网格数据。除了网格顶点和三角形之外,你可能还需要跟踪其他元数据(即使你现在不需要,将来你很可能会需要)。所以,我认为你应该仅仅将不同的组件看作是渲染网格数据的不同方式。
UCLASS(Abstract)
class RUNTIMEGEOMETRYUTILS_API ADynamicMeshBaseActor : public AActor
{
protected:
/** The SourceMesh used to initialize the mesh Components in the various subclasses */
FDynamicMesh3 SourceMesh;
};
UCLASS()
class RUNTIMEGEOMETRYUTILS_API ADynamicSMCActor : public ADynamicMeshBaseActor
{
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* MeshComponent = nullptr;
};
UCLASS()
class RUNTIMEGEOMETRYUTILS_API ADynamicPMCActor : public ADynamicMeshBaseActor
{
UPROPERTY(VisibleAnywhere)
UProceduralMeshComponent* MeshComponent = nullptr;
};
UCLASS()
class RUNTIMEGEOMETRYUTILS_API ADynamicSDMCActor : public ADynamicMeshBaseActor
{
UPROPERTY(VisibleAnywhere)
USimpleDynamicMeshComponent* MeshComponent = nullptr;
};
protected:
/**
*
Called when the SourceMesh has been modified. Subclasses override this function to
* update their respective Component with the new SourceMesh.
*
/
virtual void OnMeshEditedInternal();
对于SDMC,它只需要一个直接拷贝操作。请注意,SDMC支持直接编辑其内部的FDynamicMesh3。我本可以允许基类访问SDMC的内部网格,这可以在某些时候避免复制的开销。但是,我不会使用组件的成员数据作为我的规范源网格,我上面提过这是一个坏主意。在某些性能敏感的情况下,它可能有意义,但FDynamicMesh3的复制是非常快的,所以为了保持代码干净,我没有在这里这样做。
最后,我们在ADynamicMeshBaseActor上有一个顶层API函数来实际修改原始网格:
/**
* Call EditMesh() to safely modify the SourceMesh owned by this Actor.
* Your EditFunc will be called with the Current SourceMesh as argument,
* and you are expected to pass back the new/modified version.
* (If you are generating an entirely new mesh, MoveTemp can be used to do this without a copy)
*/
virtual void EditMesh(TFunctionRef<void(FDynamicMesh3&)> EditFunc);
基本上就是这样。如果我们有上述3个ADynamicMeshBaseActor子类中的任何一个实例,我们可以在C++中更新它,执行以下操作如下:
SomeMeshActor->EditMesh([&](FDynamicMesh3& MeshOut)
{
FDynamicMesh3 NewMesh = (...);
MeshOut = MoveTemp(NewMesh);
});
这里你可能有一个合理的对设计的质疑是,为什么这一切都在Actor上完成,而不是在组件上完成?当然,它也可以在组件上完成。这其实取决于你打算怎么处理网格。UE的一个复杂情况是,一个Actor可以有许多组件,然后如果你想要执行诸如"组合网格"等操作,你必须决定如何处理多个(可能有层级关系)的组件,其中一些组件可能不是可编辑的网格。如果你的主要处理逻辑放在组件,Actor会怎么样?(这些都是我们在设计模型编辑器模块中仍在不断挣扎的概念问题!!)对于本教程,我想将每个网格视为一个“对象”,因此通过Actors进行组织是有意义的。此外,Actor有蓝图,这将更容易做下面这些有趣的事情。
网格生成
通过这些步骤,我们现在已拥有完全动态生成和导入网格的能力。如果打开编辑器,你可以在“放置Actor”面板(通过搜索框是最简单的)中找到“Dynamic SMCActor”以及PMC和SDMC actor,将任意一个拖入场景,然后更改“详细信息视图”中的参数,网格将更新,如上方的动图所示。
蓝图API
void ADynamicMeshBaseActor::CopyFromMesh(ADynamicMeshBaseActor* OtherMesh, bool bRecomputeNormals)
{
// the part where we generate a new mesh
FDynamicMesh3 TmpMesh;
OtherMesh->GetMeshCopy(TmpMesh);
// apply our normals setting
if (bRecomputeNormals)
{
RecomputeNormals(TmpMesh);
}
// update the mesh
EditMesh([&](FDynamicMesh3& MeshToUpdate)
{
MeshToUpdate = MoveTemp(TmpMesh);
});
项目
本教程顶部的嵌入式视频快速展示了上述功能演示。我们将在下面详细介绍它们。
布尔运算!
一个小的BP提示,上面使用的“验证获取”,只有在BooleanOtherActor"Is Valid"时,才能通过右键单击正常参数获取节点并选择上下文菜单底部的“转换为已验证获取”来快速创建。我只在浪费了无数分钟去写显式的IsValid分支才得知这一点。
网格算法!
空间查询!
后两个步骤调用目标ADynamicMeshBaseActor上的ContainsPoint() 和DistanceToPoint()函数。这些只是用于如果bEnableSpatialQueies和bEnableInsideQueries属性分别为true,查询为SourceMesh自动构建的AABBTree和FastWindingTree的实用程序函数。否则,他们只会返回false。如果网格每帧更改,则构建这些数据结构可能非常昂贵,除非需要它们,否则应禁用这些结构。
还有一个ADynamicMeshBaseActor::IntersectRay()函数暴露在蓝图中,在任何示例中都未使用。你可能会发现此功能很有用,因为运行时生成的网格不一定受到LineTraces 的命中。对于PMC和SMC,它需要运行时物理烘焙,chaos并不完全支持,而且通常有些昂贵,SDMC完全不支持它。此外::IntersectRay()是基于双精度DynamicMesh3/AABBTree实现的,这对于巨大的网格是很有帮助的。(在编辑器中的许多建模模式操作期间,我们重建AABBTree,在"编辑工具"上下文中,它并不非常昂贵。)
其BP_MagnaSphere相似,只是它使用最近的点向球体添加脉冲,基本上"吸引"到兔子表面。这有时会飞向世界,所以你可能并不总是看到它。
开枪!
如上所述,每次求差集运算后,必须更新网格并重新计算AABBTree,以便能正确计算某个点到网格的距离。这听起来可能很慢,但即使我拼命对墙射击,它通常仍旧能以90-100fps在PIE中运行(在控制台中运行“stat fps”以查看帧速率)。这运行速度令人惊讶(我很惊讶!)升高墙壁或弹丸上的细化级别会对性能产生明显影响,很容易最终导致某个时候发生卡顿。需要注意的是,如果你只是点击“播放”并靠近墙体上,第一示例区域中的不断缩放的红色球体会在每帧中不断被更新,这会减慢速度(请参阅上面对PMC/SMC 讨论,红色球体是SMC)。如果禁用红色球体上的“在 Tick 上重新生成(Regenerate on Tick)”,或者先跳到其中一个布尔运算按钮(触发布尔运算后,该红球会被销毁),你就会注意到你的射击变得更流畅。
不建议经常使用“Get All Actors of Class”(尤其在频繁被调用的函数中,比如这里的Tick)……但它的确让是个非常有用并且方便的功能!
最后一点,BP_Projectile的由ARuntimeGeometryDemoCharacter的Fire()函数生成的。这当然也可以在BP完成。但注意,这是我在第三人称模板项目自动生成后的工程里唯一添加的代码——C++中的所有内容都由RuntimeGeometryutils插件完成。
碰撞呢?
然而,即使我们真需要物理解算,我们仍旧面临一个复杂的问题,因为正如我上面提到的,“简单碰撞体”是物理模拟(比如物体移动)的必备前提,而且其只允许使用球体,胶囊,盒子和凸包。因此,想要运行时物理模拟一个复杂的模型我们必须先将其表示为一组简单碰撞体的组合。想对任意复杂模型自动生成这些简单碰撞体的组合在当前的技术水平下并不可行——想想如何把上图中被兔子子弹砸烂的墙分成若干盒子和球体的组合!至于另一个选项——“复杂碰撞”,它仅适用于静态对象(即可以与之碰撞,但无法物理模拟)。鉴于烘焙和测试碰撞的成本都很高,所以可能只适合在一下小的测试或者原型场景中使用,而不是在一个游戏或者应用中大量依赖于它。
毫无疑问,这对于任何依赖于运行时生成几何的游戏或应用程序都很棘手。正如我上面展示的那样,即使没有完整的物理系统的情况下,你仍可以实现一些物理效果。并且将来完全有可能让UPrimitiveComponent的LineTrace和重叠测试能够正确支持FDynamicMeshAABBTree,这将让许多事“可行”(但物理模拟除外)。也许是未来文章的一个好主题!
后期的重大更新!
有关“异步生成简单碰撞体”选项的简短说明。这意味着烘焙是在后台线程上完成的,因此它不会阻止游戏线程。这提高了性能,但代价是需要更新的碰撞几何体不一定立即可用。因此,如果你有快速变化的几何体或快速移动的对象,它们可能会更容易地导致在碰撞完成更新之前“卡”在网格内部的情况发生。
你应该开发自己的组件吗?
但是,如果你深入到PMC代码中,你会发现PMC和SceneProxy并不复杂。不用花很长时间,你就能发现如何绕开“中间商”,实现一个你自己的网格组件,这个组件能够直接从你自己的网格格式的实例中读取数据来初始化渲染缓冲区。事实上,SDMC基本就是这么干的,当然还有一种可选择的网格格式是用FSimpleDynamicMesh3。
那么你应该这么做吗?我的观点是这取决于你想投入多少时间来保存一些网格副本。如果你有一支技术娴熟的工程师队伍,那么这不是一项巨大的工程,但随着引擎的发展,你必须不断地保持组件的更新。如果你是一支小型团队或独立团队,那么这种程度的性能优化可能不值得付出努力。我当然想分析网格复制/转换的成本,但我们的实验发现这不是瓶颈。将新的渲染缓冲区上传到GPU仍然是最费时的地方,这不会随着你采用的组件类型而改变。
如果你像本教程中那样组织“网格体系结构”,则不一定有什么影响——如果你不依赖于某个特定组件的功能,你可以根据需要换成新的组件类型。
增编:khammassi ayoub已经写了一系列非常详细的文章来介绍如何在UE里创建自己的网格组件⁵⁶⁷。然而需要注意的一个小地方是在他的教程中,大部分的工作是关于如何自定义顶点着色器,但如果你只是想做一个“类似PMC但用自己的网格格式,以避免复制”的网格组件,自定义着色器是没有必要的。尤其当你只是渲染“正常”网格,你不需要实现自己的顶点工厂/等。但这些文章仍旧非常值得一读因为其中涵盖了渲染端组件/代理基础结构的所有关键部分。
总结
虽然你可以直接在你的项目中使用ADynamicMeshBaseActor和RuntimeGeometryutils插件,但我觉这些更多的应该被视作一种指南,来帮助你建立自己的版本。如果你正在研发涉及存储运行时创建的内容的游戏或应用,我鼓励你花一些时间思考如何组织你的数据。在此演示中将源网格存储在Actor使得我后续的实现非常方便,但如果我构建的是真实内容,我就会将这些源网格的所有权从Actor转移到一些更集中的位置,例如UGameInstance子系统。你可能觉得这看起来像“可以放在稍后再去做的事儿”,但如果你基于当前的设计开发了很多蓝图或游戏功能,那之后的重构会显得非常繁琐(我最初将ADynamicMeshBaseActor放在Game目录中,后来为了能将.h/.cpp文件正确的移动到插件目录中而不破坏已有的功能,我花了一个下午来学习UE的Redirector……)
也并不是一定需要将PMC/SMC/SDMC分成不同的Actor。因为我想在演示中对其进行比较,所以这样把它们分开比较方便。但如果有一个Actor基类可以通过一个enum属性动态的生成不同的网格组件也是极好的。这可以使蓝图编程变得更加轻松。因为现在,如果你为一个Actor子类制作一个BP,并且想要将其切换到其他子类,你必须跳到好几层外面来更改其父类,并且无法在多种Actor之间共享功能(这就是为什么教程中有两个可被子弹破坏的墙的BP类,一个基于PMC,一个基于SDMC)。
最后,关于我前面提到的目前该演示无法在OSX上运行。这是因为USimpleDynamicMesh组件——它是MeshModelingToolset插件的一部分,该插件依赖于一些仅能在Windows运行的第三方模块。通过一些“#ifdef”应该可以让包含SDMC的建模组件(ModelingComponents)模块在OSX上运行起来,但这需要重新编译引擎。更直接的解决方案是删除ADynamicSDMCActor.h/.cpp和RuntimeGeometryUtils.build.cs文件中对“建模组件(ModelingComponents)”的引用,并且仅在演示工程中使用SMC或PMC的Actor。我已经在Windows上验证过,如果这样做,一切都仍然能够编译,这意味着它应该也能在OSX上工作,但我自己还没在OSX上进行测试。(请注意,这样的改动意味着大部分示例项目在不修改的前提下无法正确运行。)
[1] Geometry3Sharp
https://github.com/gradientspace/geometry3Sharp
[2] API文档链接
https://docs.unrealengine.com/en-US/API/Plugins/ProceduralMeshComponent/UProceduralMeshComponent/index.html
[3] RuntimeMeshComponent
https://github.com/TriAxis-Games/RuntimeMeshComponent
[4] MIT许可证
https://github.com/gradientspace/UnrealMeshProcessingTools
[5] 在UE4中创建自定义网格组件|第0部分
https://medium.com/realities-io/creating-a-custom-mesh-component-in-ue4-part-0-intro-2c762c5f0cd6
[6] 在UE4中创建自定义网格组件|第1部分
https://medium.com/realities-io/creating-a-custom-mesh-component-in-ue4-part-1-an-in-depth-explanation-of-vertex-factories-4a6fd9fd58f2
[7] 在UE4中创建自定义网格组件|第2部分
https://medium.com/realities-io/creating-a-custom-mesh-component-in-ue4-part-2-implementing-the-vertex-factory-4e21e51a1e10
近期焦点